###########################################################################
#
#   Copyright 2010 American Public Media Group
#
#   This file is part of AIR2.
#
#   AIR2 is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   AIR2 is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with AIR2.  If not, see <http://www.gnu.org/licenses/>.
#
###########################################################################

package AIR2::Source;
use strict;
use base qw(AIR2::DB);
use Carp;
use Data::Dump qw( dump );
use Rose::DateTime::Parser;
use Search::Tools::UTF8;

my $date_parser
    = Rose::DateTime::Parser->new( time_zone => $AIR2::Config::TIMEZONE );

__PACKAGE__->meta->setup(
    table => 'source',

    columns => [
        src_id   => { type => 'serial', not_null => 1 },
        src_uuid => {
            type     => 'character',
            length   => 12,
            not_null => 1
        },
        src_username => { type => 'varchar', length => 255, not_null => 1, },
        src_password => { type => 'character', length => 32 },
        src_first_name => { type => 'varchar', default => '', length => 64, },
        src_last_name  => { type => 'varchar', length  => 64 },
        src_middle_initial => { type => 'character', length => 1 },
        src_pre_name       => { type => 'varchar',   length => 64 },
        src_post_name      => { type => 'varchar',   length => 64 },
        src_status => { type => 'character', length => 1, default => 'A', },
        src_delete_flag => { type => 'integer', default => 0, not_null => 1 },
        src_channel  => { type => 'character', length   => 1 },
        src_cre_user => { type => 'integer',   not_null => 1 },
        src_upd_user => { type => 'integer' },
        src_cre_dtim => {
            type     => 'datetime',
            not_null => 1
        },
        src_upd_dtim => { type => 'datetime' },
    ],

    primary_key_columns => ['src_id'],

    unique_keys => [ ['src_username'], ['src_uuid'], ],

    foreign_keys => [
        cre_user => {
            class       => 'AIR2::User',
            key_columns => { src_cre_user => 'user_id' },
        },

        upd_user => {
            class       => 'AIR2::User',
            key_columns => { src_upd_user => 'user_id' },
        },
    ],

    relationships => [
        batches => {
            class      => 'AIR2::BatchSrc',
            column_map => { src_id => 'bsrc_src_id' },
            type       => 'one to many',
        },

        activities => {
            class      => 'AIR2::SrcActivity',
            column_map => { src_id => 'sact_src_id' },
            type       => 'one to many',
        },

        aliases => {
            class      => 'AIR2::SrcAlias',
            column_map => { src_id => 'sa_src_id' },
            type       => 'one to many',
        },

        annotations => {
            class      => 'AIR2::SrcAnnotation',
            column_map => { src_id => 'srcan_src_id' },
            type       => 'one to many',
        },

        emails => {
            class      => 'AIR2::SrcEmail',
            column_map => { src_id => 'sem_src_id' },
            type       => 'one to many',
        },

        facts => {
            class      => 'AIR2::SrcFact',
            column_map => { src_id => 'sf_src_id' },
            type       => 'one to many',
        },

        inquiries => {
            class      => 'AIR2::SrcInquiry',
            column_map => { src_id => 'si_src_id' },
            type       => 'one to many',
        },

        mail_addresses => {
            class      => 'AIR2::SrcMailAddress',
            column_map => { src_id => 'smadd_src_id' },
            type       => 'one to many',
        },

        media_assets => {
            class      => 'AIR2::SrcMediaAsset',
            column_map => { src_id => 'sma_src_id' },
            type       => 'one to many',
        },

        organizations => {
            map_class => 'AIR2::SrcOrg',
            map_from  => 'source',
            map_to    => 'organization',
            type      => 'many to many',
        },

        phone_numbers => {
            class      => 'AIR2::SrcPhoneNumber',
            column_map => { src_id => 'sph_src_id' },
            type       => 'one to many',
        },

        src_orgs => {
            class      => 'AIR2::SrcOrg',
            column_map => { src_id => 'so_src_id' },
            type       => 'one to many',
        },

        src_pref_org => {
            class      => 'AIR2::SrcPrefOrg',
            column_map => { src_id => 'spo_src_id' },
            type       => 'one to many',
        },

        preferences => {
            class      => 'AIR2::SrcPreference',
            column_map => { src_id => 'sp_src_id' },
            type       => 'one to many',
        },

        relationships => {
            class      => 'AIR2::SrcRelationship',
            column_map => { src_id => 'srel_src_id' },
            type       => 'one to many',
        },

        self_relationships => {
            class      => 'AIR2::SrcRelationship',
            column_map => { src_id => 'src_src_id' },
            type       => 'one to many',
        },

        responses => {
            class      => 'AIR2::SrcResponse',
            column_map => { src_id => 'sr_src_id' },
            type       => 'one to many',
        },

        response_sets => {
            class      => 'AIR2::SrcResponseSet',
            column_map => { src_id => 'srs_src_id' },
            type       => 'one to many',
        },

        uris => {
            class      => 'AIR2::SrcUri',
            column_map => { src_id => 'suri_src_id' },
            type       => 'one to many',
        },

        tags => {
            class      => 'AIR2::Tag',
            column_map => { src_id => 'tag_xid' },
            query_args => [ tag_ref_type => tag_ref_type() ],
            type       => 'one to many',
        },

        vitas => {
            class      => 'AIR2::SrcVita',
            column_map => { src_id => 'sv_src_id' },
            type       => 'one to many',
        },
    ],
);

sub get_name {
    my $self  = shift;
    my $first = $self->src_first_name || '[first name]';
    my $last  = $self->src_last_name || '[last name]';
    return "$last, $first";
}

sub tag_ref_type {'S'}

sub init_indexer {
    my $self = shift;
    return $self->SUPER::init_indexer(
        prune => {

        },
        max_depth        => 1,
        xml_root_element => 'source',
        force_load       => 0,
        @_
    );
}

my @indexables = qw(
    src_orgs
    emails
    phone_numbers
    mail_addresses
    vitas
    facts
    preferences
);

my @searchables = (
    @indexables, qw(
        activities
        response_sets
        inquiries
        annotations
        tags
        )
);

sub get_searchable_rels { return [@searchables] }

my %authz;

sub get_authz {
    my $self = shift;
    if ( exists $authz{ $self->src_id } ) {
        return $authz{ $self->src_id };
    }
    my @ids;
    for my $so ( @{ $self->src_orgs } ) {
        if ( $so->so_status ne 'A' ) {
            next;
        }
        $so->organization->collect_related_org_ids( \@ids );
    }
    my %uniq = map { $_ => $_ } @ids;
    $authz{ $self->src_id } = [ sort { $a <=> $b } keys %uniq ];
    return $authz{ $self->src_id };
}

sub load_indexable_rels {
    my $self = shift;
    for my $rel (@indexables) {
        $self->$rel;
    }
}

sub get_primary_email {
    my $self = shift;
    for my $email ( @{ $self->emails } ) {
        if ( $email->sem_primary_flag ) {
            return $email;
        }
    }
    return;
}

sub get_primary_address {
    my $self = shift;
    for my $addr ( @{ $self->mail_addresses } ) {
        if ( $addr->smadd_primary_flag ) {
            return $addr;
        }
    }
    return;
}

sub num_involved_activities {

    croak "TODO";

    my $self = shift;
    my $n    = 0;
    for my $cact ( @{ $self->con_activitys } ) {
        if (   $cact->is_response
            or $cact->cact_actm_id == 23
            or $cact->cact_actm_id == 24 )
        {
            $n++;
        }
    }
    return $n;
}

sub count_incoming_activities {

    croak "TODO";

    my $self = shift;
    my $n    = 0;
    for my $cact ( @{ $self->con_activitys } ) {
        if ( $cact->is_incoming ) {
            $n++;
        }
    }
    return $n;
}

sub outgoing_activity_text {

    croak "TODO";

    my $self = shift;
    my @text;
    for my $cact ( @{ $self->con_activitys } ) {
        if ( $cact->is_outgoing ) {
            push @text, ( $cact->cact_action_detail || '' ),
                ( $cact->cact_action_notes || '' );
        }
    }
    return @text;
}

sub last_contacted {
    my $self = shift;
    my $inq  = $self->inquiries_iterator( sort_by => 'si_cre_dtim DESC' );
    my $last = $inq->next;
    if ($last) {
        return $last->si_cre_dtim->epoch;
    }

    # if we didn't have *any* inquiries, use the activity log instead.
    # this is a AIR1->AIR2 conversion issue, since the AIR2 src_inquiry
    # table is empty on Day One of AIR2 deployment.
    my $acts = $self->activities_iterator( sort_by => 'sact_dtim DESC' );
    while ( my $act = $acts->next ) {
        if ( $act->get_master->is_outgoing ) {
            $last = $act;
            last;
        }
    }
    if ($last) {
        return $last->sact_dtim->epoch;
    }
    return;
}

sub last_contacted_date {
    my $self  = shift;
    my $epoch = $self->last_contacted or return 0;
    my $dt    = $date_parser->parse_date($epoch)
        or croak "can't parse date $epoch";
    return $dt->ymd('');
}

sub last_response {
    my $self = shift;
    my $resp = $self->responses_iterator( sort_by => 'sr_cre_dtim DESC' );
    my $last = $resp->next;
    if ($last) {
        return $last->sr_cre_dtim->epoch;
    }
    return;
}

sub last_response_date {
    my $self  = shift;
    my $epoch = $self->last_response or return 0;
    my $dt    = $date_parser->parse_date($epoch)
        or croak "can't parse date $epoch";
    return $dt->ymd('');
}

sub first_response {
    my $self  = shift;
    my $resp  = $self->responses_iterator( sort_by => 'sr_cre_dtim ASC' );
    my $first = $resp->next;
    if ($first) {
        return $first->sr_cre_dtim->epoch;
    }
    return;
}

sub last_activity_date {
    my $self = shift;
    my $acts = $self->activities_iterator( sort_by => 'sact_dtim DESC' );
    my $last = $acts->next;
    if ($last) {
        return $last->sact_dtim->ymd('');
    }
    return 0;
}

sub first_response_date {
    my $self  = shift;
    my $epoch = $self->first_response or return 0;
    my $dt    = $date_parser->parse_date($epoch)
        or croak "can't parse date $epoch";
    return $dt->ymd('');
}

sub stem_names {
    my $self = shift;
    my @stems;

    # forwards and backwards, down to a single letter.
    for my $col (qw( src_first_name src_last_name )) {
        my @val = split( m//, $self->$col );
        my @copy = @val;
        while ( pop @val && @val ) {
            push( @stems, join( '', @val ) );
        }

        while ( shift @copy && @copy ) {
            push( @stems, join( '', @copy ) );
        }
    }

    return @stems;

}

=head2 as_xml( I<args> )

Returns Source as XML string, suitable for indexing.

I<args> should contain a Rose::DBx::Object::Indexed::Indexer
object and other objects relevant to the XML structure.
See bin/sources2xml.pl for example usage.

=cut

sub as_xml {
    my $source  = shift;
    my $args    = shift or croak "args required";
    my $debug   = delete $args->{debug} || 0;
    my $indexer = delete $args->{indexer}
        || $source->init_indexer( debug => $debug, );
    my $base_dir = delete $args->{base_dir}
        || Path::Class::dir('no/such/dir');
    my $organizations = delete $args->{organizations}
        || AIR2::SearchUtils::all_organizations_by_id();
    my $fact_values = delete $args->{fact_values}
        || AIR2::SearchUtils::all_fact_values_by_id();
    my $facts = delete $args->{facts}
        || AIR2::SearchUtils::all_facts_by_id();
    my $prefs = delete $args->{pref_values}
        || AIR2::SearchUtils::all_preference_values_by_id();

    # loading each rel one-at-a-time is slower than a 'with' clause in load()
    # for sources with just a couple of responses,
    # but magnitudes faster for sources with many responses.
    $source->load_indexable_rels;

    # turn object into a hash tree
    my $dmp = $indexer->serialize_object($source);

    $debug and dump $dmp;

    # $debug and dump $activity_responses;

    # pseudo-tags for filtering
    for my $org ( @{ $dmp->{src_orgs} } ) {

        $org->{org} = $organizations->{ $org->{so_org_id} }->org_name;
        $org->{org_status} = join( '_', $org->{org}, $org->{so_status} );

        # create a string that captures: dgname, status and mod_date
        # so that we can filter on it.
        my $upd
            = AIR2::SearchUtils::dtim_string_to_ymd( $org->{so_upd_dtim} );
        $org->{org_status_date} = join( '_', $org->{org_status}, $upd );

    }

    # pseudo-tags for emails
    for my $email ( @{ $dmp->{emails} } ) {
        if ( $email->{sem_primary_flag} ) {
            $dmp->{primary_email} = $email->{sem_email};
        }
    }

    # pseudo-tags for primary address and phone
    for my $phone ( @{ $dmp->{phone_numbers} } ) {
        if ( defined $phone->{sph_primary_flag}
            and $phone->{sph_primary_flag} )
        {
            $dmp->{primary_phone} = join( ' ',
                grep {defined} $phone->{sph_number},
                $phone->{sph_ext} );
        }

    }
    for my $addr ( @{ $dmp->{mail_addresses} } ) {

        next unless $addr->{smadd_primary_flag};
        $dmp->{primary_city}    = $addr->{smadd_city};
        $dmp->{primary_state}   = $addr->{smadd_state};
        $dmp->{primary_zip}     = $addr->{smadd_zip};
        $dmp->{primary_country} = $addr->{smadd_cntry};
    }

    # vitas
    for my $vita ( @{ $dmp->{vitas} } ) {
        if ( $vita->{sv_type} eq 'I' ) {
            $vita->{interest} = $vita->{sv_notes};
        }
        elsif ( $vita->{sv_type} eq 'E' ) {
            $vita->{experience}
                = join( "::", $vita->{sv_basis}, $vita->{sv_value} );
            if ( defined $vita->{sv_start_date} ) {
                $vita->{experience_start}
                    = AIR2::SearchUtils::dtim_string_to_ymd(
                    $vita->{sv_start_date} );
            }
            if ( defined $vita->{sv_end_date} ) {
                $vita->{experience_end}
                    = AIR2::SearchUtils::dtim_string_to_ymd(
                    $vita->{sv_end_date} );
            }
        }
    }

    # flatten preferences by field-like name and human value.
    for my $p ( @{ $dmp->{preferences} } ) {
        my $pref = $prefs->{ $p->{sp_ptv_id} };
        my $name = lc( $pref->preference_type->pt_name );
        $name =~ s/\W+/_/g;
        $p->{$name} = $pref->ptv_value;
    }

    my %response_sets;
    for my $srs ( @{ $source->response_sets } ) {
        my $srs_id = $srs->srs_id;
        my $path   = AIR2::SearchUtils::xml_path_for( $srs_id,
            $base_dir->subdir( '..', 'responses' ) );

        if ( !-s "$path" ) {

            #warn "no file at $path";
            #next;
        }
        $response_sets{$srs_id} = $path;
    }

    # TODO prj_uuid for "related projects"?
    # where "related" via inquiry (sent, responded to, in a bucket related to)

    my %activities;
    for my $sact ( @{ $source->activities } ) {
        my $sact_id = $sact->sact_id;
        my $path    = AIR2::SearchUtils::xml_path_for( $sact_id,
            $base_dir->subdir( '..', 'activities' ) );

        # ok for it not to exist yet. might be creating xml asynchronously.
        if ( !-s "$path" ) {

            #warn "no file at $path";
            #next;
        }
        $activities{$sact_id} = $path;
    }

    for my $src_fact ( @{ $dmp->{facts} } ) {

        # could be 0, 1 or 2 FactValues for each SrcFact
        my ( $user_fact_value, $src_fact_value );

        if ( defined $src_fact->{sf_src_fv_id}
            and exists $fact_values->{ $src_fact->{sf_src_fv_id} } )
        {
            $src_fact_value = $fact_values->{ $src_fact->{sf_src_fv_id} };
        }
        if ( defined $src_fact->{sf_fv_id}
            and exists $fact_values->{ $src_fact->{sf_fv_id} } )
        {
            $user_fact_value = $fact_values->{ $src_fact->{sf_fv_id} };
        }

        # analyst-mapped values
        if ($user_fact_value) {
            $src_fact->{ 'user_' . $user_fact_value->fact->fact_identifier }
                = $user_fact_value->fv_value;
            $src_fact->{'user_fact'} = join( '_',
                $user_fact_value->fv_fact_id,
                $user_fact_value->fv_id );
        }

        # source-mapped values
        if ($src_fact_value) {
            $src_fact->{ 'src_' . $src_fact_value->fact->fact_identifier }
                = $src_fact_value->fv_value;
            $src_fact->{'src_fact'} = join( '_',
                $src_fact_value->fv_fact_id, $src_fact_value->fv_id );
        }

        # source-unmapped (raw) values
        if ( defined $src_fact->{sf_src_value}
            and length $src_fact->{sf_src_value} )
        {
            $src_fact->{ $facts->{ $src_fact->{sf_fact_id} }
                    ->fact_identifier } = $src_fact->{sf_src_value};
        }

        $debug and dump($src_fact);

    }

    # more virtual fields
    for my $f (
        qw(
        last_contacted_date
        last_response_date
        first_response_date
        last_activity_date
        )
        )
    {
        $dmp->{$f} = $source->$f;
    }

    $dmp->{tags} = [ map { $_->get_name } @{ $source->get_tags } ];

    $dmp->{source_name} = join( ' ',
        grep {defined} $source->src_first_name,
        $source->src_last_name, $source->src_first_name )
        . ' '
        . join( ' ', $source->stem_names );

    my $xml = $indexer->to_xml( $dmp, $source );
    my $root = $indexer->xml_root_element;

    # hack in the authz string
    my $authz_str = join( ",", @{ $source->get_authz } );
    $xml =~ s,^<$root,<$root authz="$authz_str",;

    # to conserve processing time and disk space,
    # we use xinclude to virtually include records we have
    # serialized in other processes.
    $xml =~ s,^<$root,<$root xmlns:xi="http://www.w3.org/2003/XInclude",;

    # add the xincludes for response sets
    my $xml_fragment = sprintf( '<response_sets count="%d">',
        scalar( keys %response_sets ) );
    for my $srs_id ( sort keys %response_sets ) {
        $xml_fragment .= qq(<xi:include href="$response_sets{$srs_id}"/>);
    }
    $xml_fragment .= qq(</response_sets>);

    # same for activities
    $xml_fragment
        .= sprintf( '<activities count="%d">', scalar( keys %activities ) );
    for my $sact ( sort keys %activities ) {
        $xml_fragment .= qq(<xi:include href="$activities{$sact}"/>);
    }
    $xml_fragment .= qq(</activities>);

    $xml =~ s,</$root>,$xml_fragment</$root>,;

    return $xml;
}

1;

